﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Gert3D
{
  public class RaytracerVoxeled : IRenderer
  {
    private List<Mesh> _meshes = null;
    private List<VoxelGrid> _voxelGrids = null;
    private List<PointLight> _lights = null;

    public bool Initialize(List<Mesh> meshes, List<PointLight> lights)
    {
      _meshes = meshes;
      _voxelGrids = new List<VoxelGrid>();
      foreach (Mesh mesh in _meshes)
      {
        VoxelGrid grid = new VoxelGrid();
        grid.Voxelize(mesh);
        _voxelGrids.Add(grid);
      }

      _lights = lights;
      return true;
    }

    public bool RenderRegion(Rectangle2 region, Ray[,] rays, Color4[,] result)
    {
      int left = (int)region.Left;
      int right = (int)region.Right;
      int top = (int)region.Top;
      int bottom = (int)region.Bottom;

      Color4 color;

      for (int y = top; y <= bottom; y++)
      {
        for (int x = left; x <= right; x++)
        {
          double minT = double.MaxValue; //Used to exit early from intersection test
          IntersectionInfo closestIntersection = null; //The intersection information of the closest intersection found so far
          Ray eyeRay = rays[x, y];
          //Find closest intersection
          foreach (VoxelGrid grid in _voxelGrids)
          {
            IntersectionInfo info = grid.IntersectRay(eyeRay);
            if (info != null)
            {
              minT = info.T;
              closestIntersection = info;
            }
          }

          //Initialize color
          color.R = 0;
          color.G = 0;
          color.B = 0;
          color.A = 0;

          if (closestIntersection != null)
          {
            color.A = 1.0;
            // Calcluate lighting
            foreach (PointLight light in _lights)
            {
              Ray lightRay = Ray.FromPoints(closestIntersection.IntersectionPoint, light.Position);
              double mintLightT = double.MaxValue;
              bool shadow = false;

              //Check for shadow
              foreach (VoxelGrid grid in _voxelGrids)
              {
                IntersectionInfo info = grid.IntersectRay(lightRay);
                if (info != null)
                {
                  mintLightT = info.T;
                  shadow = true;
                  break;
                }
              }

              if (!shadow)
              {
                /*color.R = 1.0;
                color.G = 1.0;
                color.B = 1.0;*/

                //Calculate diffuse lambert lighting
                double diffuse = Lambert(closestIntersection.Normal, lightRay.Direction) * light.Intensity;
                //double diffuse = OrenNayar(closestIntersection.Normal, lightRay.Direction, eyeRay.Origin, closestIntersection.IntersectionPoint, 0.8) * light.Intensity;
                color.R += light.Color.R * diffuse;
                color.G += light.Color.G * diffuse;
                color.B += light.Color.B * diffuse;

                /*Color4 lighting = CookTorrance(
                  closestIntersection.Normal,
                  Vector3.Normalize(Vector3.FromPoints(closestIntersection.IntersectionPoint, eyeRay.Origin)),
                  Vector3.Normalize(Vector3.FromPoints(closestIntersection.IntersectionPoint, light.Position)),
                  0.0,
                  1,
                  light.Color,
                  light.Color);*/

                /*Color4 lighting = Strauss(
                  closestIntersection.Normal,
                  light.Position,
                  eyeRay.Origin,
                  closestIntersection.IntersectionPoint,
                  light.Color,
                  0.6,
                  0,
                  0);*/
                /*color.R += lighting.R;
                color.G += lighting.G;
                color.B += lighting.B;*/
              }
            }
          }
          result[x, y] = color;

        }
      }

      return true;
    }

    private double Lambert(Vector3 normal, Vector3 lightDirection)
    {
      return Math.Max(0.0, Vector3.DotProduct(normal, lightDirection));
    }

    private double OrenNayar(Vector3 n, Vector3 light, Vector3 origin, Vector3 pos, double roughness)
    {
      Vector3 v = Vector3.FromPoints(origin, pos);
      v.Normalize();
      Vector3 l = Vector3.Normalize(light);
      //n.Normalize();

      Vector3 vn = Vector3.FromPoints(v, n);
      vn.Normalize();
      Vector3 ln = Vector3.FromPoints(l, n);
      ln.Normalize();

      double dotvn = Vector3.DotProduct(v, n);
      double dotln = Vector3.DotProduct(l, n);

      double gamma = 
        Vector3.DotProduct(
          Vector3.Scale(vn, dotvn), Vector3.Scale(ln, dotln));

      double rough_sq = roughness * roughness;

      double A = 1.0 - 0.5 * (rough_sq / (rough_sq + 0.57));

      double B = 0.45 * (rough_sq / (rough_sq + 0.09));

      double C;
      double alpha = Math.Max(Math.Acos(dotvn), Math.Acos(dotln));
      double beta = Math.Min(Math.Acos(dotvn), Math.Acos(dotln));

      C = Math.Sin(alpha) * Math.Tan(beta);

      double final = (A + B * Math.Max(0.0, gamma) * C);

      return Math.Max(0.0, dotln) * final;
      //return Math.Max(0.0, Vector3.DotProduct(n, l)) * final;

      /*
        // Make sure the interpolated inputs and
        // constant parameters are normalized
        float3 n = normalize( f.normal );
        float3 l = normalize( -vLightDirection );
        float3 v = normalize( pCameraPosition - f.world );
     
        // Compute the other aliases
        float gamma   = dot
                        ( 
                            v - n * dot( v, n ), 
                            l - n * dot( l, n ) 
                        );
     
        float rough_sq = fRoughness * fRoughness;
     
        float A = 1.0f - 0.5f * (rough_sq / (rough_sq + 0.57f));
     
        float B = 0.45f * (rough_sq / (rough_sq + 0.09));
     
        float C;
        if( UseLookUpTexture )
        {
            // The two dot-products will be in the range of 
            // 0.0 to 1.0 which is perfect for a texture lookup:
            float tc = float2
                        ( 
                            (VdotN + 1.0f) / 2.0f, 
                            (LdotN + 1.0f) / 2.0f 
                        );
	    C = texSinTanLookup.Sample( DefaultSampler, tc ).r;
        }
        else
        {
            float alpha = max( acos( dot( v, n ) ), acos( dot( l, n ) ) );
            float beta  = min( acos( dot( v, n ) ), acos( dot( l, n ) ) );
     
            C = sin(alpha) * tan(beta);
        }
     
        float3 final = (A + B * max( 0.0f, gamma ) * C);
     
        return float4( cDiffuse * max( 0.0f, dot( n, l ) ) * final, 1.0f );

       */
    }

    private double Saturate(double value)
    {
      if (value > 1.0)
        return 1.0;
      else if (value < 0.0)
        return 0.0;
      else
        return value;
    }

    private Color4 CookTorrance(Vector3 normal, Vector3 viewer, Vector3 light, double roughnessValue, double ior, Color4 lightColor, Color4 diffuseColor)
    {
      if (roughnessValue == 0.0)
        roughnessValue = 0.0000001;

      Vector3 half_vector = Vector3.Add(light, viewer);
      half_vector.Normalize();
      double NdotL = Saturate(Vector3.DotProduct(normal, light));
      double NdotH = Saturate(Vector3.DotProduct(normal, half_vector));
      double NdotV = Saturate(Vector3.DotProduct(normal, viewer));
      double VdotH = Saturate(Vector3.DotProduct(viewer, half_vector));
      double r_sq = roughnessValue * roughnessValue;
      
      // Evaluate the geometric term
      // --------------------------------
      double geo_numerator = 2.0 * NdotH;
      double geo_denominator = VdotH;

      double geo_b = (geo_numerator * NdotV) / geo_denominator;
      double geo_c = (geo_numerator * NdotL) / geo_denominator;
      double geo = Math.Min(1.0, Math.Min(geo_b, geo_c));



      // Now evaluate the roughness term
      // -------------------------------
      /*double roughness_a = 1.0f / (4.0f * r_sq * Math.Pow(NdotH, 4));
      double roughness_b = NdotH * NdotH - 1.0f;
      double roughness_c = r_sq * NdotH * NdotH;
      double roughness = roughness_a * Math.Exp(roughness_b / roughness_c);*/

      // This variable could be exposed as a variable
      // for the application to control:
      double c = 1.0f;
      double alpha = Math.Acos(Vector3.DotProduct(normal, half_vector));
      double roughness = c * Math.Exp(-(alpha / r_sq));

      // Next evaluate the Fresnel value
      // -------------------------------
      double fresnel = Math.Pow(1.0f - VdotH, 5.0f);
      fresnel *= (1.0f - ior);
      fresnel += ior;

      // Put all the terms together to compute
      // the specular term in the equation
      // -------------------------------------
      double Rs_numerator = (fresnel * geo * roughness);
      double Rs_denominator = NdotV * NdotL;
      double Rs = Rs_numerator / Rs_denominator;

      // Put all the parts together to generate
      // the final colour
      // --------------------------------------

      Color4 result = new Color4();
      result.R = Math.Max(0.0f, NdotL) * (lightColor.R * Rs + diffuseColor.R);
      result.G = Math.Max(0.0f, NdotL) * (lightColor.G * Rs + diffuseColor.G);
      result.B = Math.Max(0.0f, NdotL) * (lightColor.B * Rs + diffuseColor.B);

      return result;

      /*float4 cook_torrance
        (
            in float3 normal,
            in float3 viewer,
            in float3 light,
            uniform int roughness_mode
        )
      {    
          // Compute any aliases and intermediary values
          // -------------------------------------------
          float3 half_vector = normalize( light + viewer );
          float NdotL        = saturate( dot( normal, light ) );
          float NdotH        = saturate( dot( normal, half_vector ) );
          float NdotV        = saturate( dot( normal, viewer ) );
          float VdotH        = saturate( dot( viewer, half_vector ) );
          float r_sq         = roughness_value * roughness_value;
       
       
       
          // Evaluate the geometric term
          // --------------------------------
          float geo_numerator   = 2.0f * NdotH;
          float geo_denominator = VdotH;
       
          float geo_b = (geo_numerator * NdotV ) / geo_denominator;
          float geo_c = (geo_numerator * NdotL ) / geo_denominator;
          float geo   = min( 1.0f, min( geo_b, geo_c ) );
       
       
       
          // Now evaluate the roughness term
          // -------------------------------
          float roughness;
       
          if( ROUGHNESS_LOOK_UP == roughness_mode )
          {
              // texture coordinate is:
              float2 tc = { NdotH, roughness_value };
       
              // Remap the NdotH value to be 0.0-1.0
              // instead of -1.0..+1.0
              tc.x += 1.0f;
              tc.x /= 2.0f;
       
              // look up the coefficient from the texture:
              roughness = texRoughness.Sample( sampRoughness, tc );
          }
          if( ROUGHNESS_BECKMANN == roughness_mode )
          {        
              float roughness_a = 1.0f / ( 4.0f * r_sq * pow( NdotH, 4 ) );
              float roughness_b = NdotH * NdotH - 1.0f;
              float roughness_c = r_sq * NdotH * NdotH;
       
              roughness = roughness_a * exp( roughness_b / roughness_c );
          }
          if( ROUGHNESS_GAUSSIAN == roughness_mode )
          {
              // This variable could be exposed as a variable
              // for the application to control:
              float c = 1.0f;
              float alpha = acos( dot( normal, half_vector ) );
              roughness = c * exp( -( alpha / r_sq ) );
          }
       
       
       
          // Next evaluate the Fresnel value
          // -------------------------------
          float fresnel = pow( 1.0f - VdotH, 5.0f );
          fresnel *= ( 1.0f - ref_at_norm_incidence );
          fresnel += ref_at_norm_incidence;
       
       
       
          // Put all the terms together to compute
          // the specular term in the equation
          // -------------------------------------
          float3 Rs_numerator   = ( fresnel * geo * roughness );
          float Rs_denominator  = NdotV * NdotL;
          float3 Rs             = Rs_numerator/ Rs_denominator;
       
       
       
          // Put all the parts together to generate
          // the final colour
          // --------------------------------------
          float3 final = max(0.0f, NdotL) * (cSpecular * Rs + cDiffuse);
       
       
       
          // Return the result
          // -----------------
          return float4( final, 1.0f );
      }*/
    }

    private double Fresnel(double x)
    {
      double kf = 1.12;
      double temp = 1 / (kf * kf);
      return ((1 / Math.Pow(x - kf, 2)) - temp) / ((1 / Math.Pow(1 - kf, 2)) - temp);
    }

    private double Shadow(double x)
    {
      double ks = 1.01;
      double temp = 1 / Math.Pow(1 - ks, 2);
      return (temp - (1 / Math.Pow(x - ks, 2))) / (temp - (1 / (ks * ks)));
    }

    public Color4 Strauss(Vector3 normal, Vector3 light, Vector3 camera, Vector3 position, Color4 diffuseColor, double smoothness, double metalness, double transparency)
    {
      // Make sure the interpolated inputs and
      // constant parameters are normalized
      Vector3 n = Vector3.Normalize(normal);
      Vector3 l = Vector3.Normalize(Vector3.FromPoints(position, light));
      Vector3 v = Vector3.Normalize(Vector3.FromPoints(position, camera));
      Vector3 h = Vector3.Reflect(l, n);

      // Declare any aliases:
      double NdotL = Vector3.DotProduct(n, l);
      double NdotV = Vector3.DotProduct(n, v);
      double HdotV = Vector3.DotProduct(h, v);
      double fNdotL = Fresnel(NdotL);
      double s_cubed = smoothness * smoothness * smoothness;

      // Evaluate the diffuse term
      double d = (1.0f - metalness * smoothness);
      double Rd = (1.0f - s_cubed) * (1.0f - transparency);
      Color4 diffuse = new Color4();
      diffuse.R = NdotL * d * Rd * diffuseColor.R;
      diffuse.G = NdotL * d * Rd * diffuseColor.G;
      diffuse.B = NdotL * d * Rd * diffuseColor.B;

      // Compute the inputs into the specular term
      double r = (1.0f - transparency) - Rd;

      double j = fNdotL * Shadow(NdotL) * Shadow(NdotV);

      // 'k' is used to provide small off-specular
      // peak for very rough surfaces. Can be changed
      // to suit desired results...
      const double k = 0.1f;
      double reflect = Math.Min(1.0, r + j * (r + k));

      Color4 C1 = new Color4(1.0, 1.0, 1.0);
      Color4 Cs = new Color4();
      Cs.R = C1.R + metalness * (1.0 - fNdotL) * (diffuseColor.R - C1.R);
      Cs.G = C1.G + metalness * (1.0 - fNdotL) * (diffuseColor.G - C1.G);
      Cs.B = C1.B + metalness * (1.0 - fNdotL) * (diffuseColor.B - C1.B);

      // Evaluate the specular term
      Color4 specular = new Color4();
      specular.R = Cs.R * reflect;
      specular.G = Cs.G * reflect;
      specular.B = Cs.B * reflect;
      double mul = Math.Pow(-HdotV, 3.0 / (1.0 - smoothness));
      specular.R *= mul;
      specular.G *= mul;
      specular.B *= mul;

      // Composite the final result, ensuring
      // the values are >= 0.0f yields better results. Some
      // combinations of inputs generate negative values which
      // looks wrong when rendered...
      diffuse.R = Math.Max(0.0, diffuse.R);
      diffuse.G = Math.Max(0.0, diffuse.G);
      diffuse.B = Math.Max(0.0, diffuse.B);

      specular.R = Math.Max(0.0, specular.R);
      specular.G = Math.Max(0.0, specular.G);
      specular.B = Math.Max(0.0, specular.R);

      return new Color4(
        diffuse.R + specular.R,
        diffuse.G + specular.G,
        diffuse.B + specular.B);

      //return float4(diffuse + specular, 1.0);

    }

  }
}
